/*******************************************************************************
 * Copyright 2008 www.softao.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package com.softao.ming.gwt.user.client;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
import com.softao.ming.gwt.user.client.layout.PatternLayout;
import com.softao.ming.gwt.user.client.util.PatternReader;

/**
 * The {@link GridLayout} pattern looks like:
 * 
 * <pre>
 * |[CellType]{[StyleName]}|$|[CellType]{[StyleName]}|
 * </pre>
 * 
 * The pattern definition is:
 * <ul>
 * <li><code>|</code>: left and/ or right line of the cell.</li>
 * <li><code>[CellType]</code>: type of the cell, this is mandatory.</li>
 * <li><code>{[StyleName]}</code>: style name of the cell, this is optional.</li>
 * <li><code>$</code>: new line tag.</li>
 * </ul>
 * 
 * Currently, the <code>CellType</code> is one of:
 * <ul>
 * <li><code>@</code>: anchor position, where <code>text</code>,
 * <code>HTML</code> and <code>widget</code> can be put.</li>
 * <li><code>&lt;</code>: indicates that the cell will be merged with the left
 * cell, (if its left cell is also the same type, both will be merged to left's
 * left cell).</li>
 * <li><code>&circ;</code>: indicates that the cell will be merged with the
 * above cell, (if its above cell is the same type, both will be merged to
 * above's above cell).</li>
 * <li><code>&nbsp;</code>(a single space): is a place holder, where no widgets
 * can be put.</li>
 * </ul>
 * 
 * <b>For example</b>:
 * 
 * <pre>
 * |@{test-style1}|&lt;|@|$| |@{test-style2}|&circ;|
 * </pre>
 * 
 * It defines the layout looks like:
 * 
 * <pre>
 * | cell1   cell1 | cell2 |
 * |       | cell3 | cell2 |
 * </pre>
 * 
 * @author Dop Sun Jul 11, 2008
 */
public final class GridLayout extends Composite implements PatternLayout {

	private static class GridCellDefinition {

		private String mCellType;

		private int mRow;

		private int mCol;

		private String mStyleName;

		private int mRowSpan;

		private int mColSpan;

		private String mWidth;
		
		private String mHeight;
		
		/**
		 * Initializes a new instance of
		 * <code>GridLayout.GridCellDefinition</code>.
		 */
		public GridCellDefinition() {
			this.mRowSpan = 1;
			this.mColSpan = 1;
			this.mWidth = "";
			this.mHeight = "";
		}

		/**
		 * @return the cellType
		 */
		public final String getCellType() {
			return mCellType;
		}

		/**
		 * @return the column
		 */
		public final int getCol() {
			return mCol;
		}

		/**
		 * @return the colSpan
		 */
		public final int getColSpan() {
			return mColSpan;
		}

		/**
		 * @return the row
		 */
		public final int getRow() {
			return mRow;
		}

		/**
		 * @return the rowSpan
		 */
		public final int getRowSpan() {
			return mRowSpan;
		}

		/**
		 * @return the className
		 */
		public final String getStyleName() {
			String styleName = this.mStyleName;
			if (styleName != null && styleName.length() < 2) {
				if ("L".equals(styleName)) {
					return "ming-gridLayoutLabel";
				} else if ("T".equals(styleName)) {
					return "ming-gridLayoutTextBox";
				} else if ("B".equals(styleName)) {
					return "ming-gridLayoutButton";
				} else if ("C".equals(styleName)) {
					return "ming-gridLayoutCheckBox";
				}
			}
			return styleName;
		}

		/**
		 * @param cellType
		 *            the cellType to set
		 */
		public void setCellType(String cellType) {
			mCellType = cellType;
		}

		/**
		 * @param col
		 *            the col to set
		 */
		public void setCol(int col) {
			mCol = col;
		}

		/**
		 * @param colSpan
		 *            the colSpan to set
		 */
		public void setColSpan(int colSpan) {
			if (colSpan < 0) {
				String msg = "colSpan can not be negative.";
				throw new IndexOutOfBoundsException(msg);
			}
			mColSpan = colSpan;
		}

		/**
		 * @param row
		 *            the row to set
		 */
		public void setRow(int row) {
			mRow = row;
		}

		/**
		 * @param rowSpan
		 *            the rowSpan to set
		 */
		public final void setRowSpan(int rowSpan) {
			if (rowSpan < 0) {
				String msg = "rowSpan can not be negative.";
				throw new IndexOutOfBoundsException(msg);
			}
			mRowSpan = rowSpan;
		}

		/**
		 * @param styleName
		 *            the styleName to set
		 */
		public void setStyleName(String styleName) {
			mStyleName = styleName;
		}

		/**
		 * @param width the width to set
		 */
		public void setWidth(String width) {
			mWidth = width;
		}

		/**
		 * @return the width
		 */
		public String getWidth() {
			return mWidth;
		}

		/**
		 * @param height the height to set
		 */
		public void setHeight(String height) {
			mHeight = height;
		}

		/**
		 * @return the height
		 */
		public String getHeight() {
			return mHeight;
		}
	}

	private static class GridLayoutPosition {
		private final int mRow;

		private final int mColumn;

		/**
		 * Initializes a new instance of
		 * <code>GridLayout.GridLayoutPosition</code>.
		 * 
		 * @param row
		 *            the row index
		 * @param column
		 *            the col index
		 */
		public GridLayoutPosition(int row, int column) {
			if (row < 0 || column < 0) {
				String msg = "row and column can not be negative.";
				throw new IndexOutOfBoundsException(msg);
			}

			this.mRow = row;
			this.mColumn = column;
		}

		/**
		 * @return the column index of the position
		 */
		public final int getColumn() {
			return mColumn;
		}

		/**
		 * @return the row index of the position
		 */
		public final int getRow() {
			return mRow;
		}
	}

	private final String mPattern;

	private final List<GridLayoutPosition> mPositions;

	private final FlexTable pnlLayout;

	/**
	 * Initializes a new instance of <code>GridLayout</code>.
	 * 
	 * @param pattern
	 *            grid layout patter to be set.
	 * @throws InvalidPatternException
	 *             if pattern is invalid
	 */
	public GridLayout(String pattern) throws InvalidPatternException {
		this.pnlLayout = new FlexTable();
		this.pnlLayout.setCellPadding(0);
		this.pnlLayout.setCellSpacing(0);
		this.pnlLayout.setStyleName("ming-gridLayout");

		this.mPositions = new ArrayList<GridLayoutPosition>();

		this.mPattern = pattern;
		this.parsePattern(pattern);

		this.initWidget(this.pnlLayout);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.softao.ming.gwt.user.client.PatternLayout#getHTML(int)
	 */
	public String getHTML(int position) throws IndexOutOfBoundsException {
		GridLayoutPosition location = this.mPositions.get(position);
		return this.pnlLayout.getHTML(location.getRow(), location.getColumn());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.softao.ming.gwt.user.client.PatternLayout#getLayoutWidget()
	 */
	public final Widget getLayoutWidget() {
		return this.pnlLayout;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.softao.ming.gwt.user.client.PatternLayout#getPattern()
	 */
	public final String getPattern() {
		return this.mPattern;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.softao.ming.gwt.user.client.PatternLayout#getPositionCount()
	 */
	public final int getPositionCount() {
		return this.mPositions.size();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.softao.ming.gwt.user.client.PatternLayout#getText(int)
	 */
	public final String getText(int position) throws IndexOutOfBoundsException {
		GridLayoutPosition location = this.mPositions.get(position);
		return this.pnlLayout.getText(location.getRow(), location.getColumn());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.softao.ming.gwt.user.client.PatternLayout#getWidget(int)
	 */
	public final Widget getWidget(int position)
			throws IndexOutOfBoundsException {
		GridLayoutPosition location = this.mPositions.get(position);
		return this.pnlLayout.getWidget(location.getRow(), location.getColumn());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.softao.ming.gwt.user.client.PatternLayout#setHTML(int,
	 * java.lang.String)
	 */
	public final void setHTML(int position, String html)
			throws IndexOutOfBoundsException {
		GridLayoutPosition location = this.mPositions.get(position);
		this.pnlLayout.setHTML(location.getRow(), location.getColumn(), html);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.softao.ming.gwt.user.client.PatternLayout#setText(int,
	 * java.lang.String)
	 */
	public final void setText(int position, String text)
			throws IndexOutOfBoundsException {
		GridLayoutPosition location = this.mPositions.get(position);
		this.pnlLayout.setText(location.getRow(), location.getColumn(), text);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.softao.ming.gwt.user.client.PatternLayout#setWidget(int,
	 * com.google.gwt.user.client.ui.Widget)
	 */
	public final void setWidget(int position, Widget widget)
			throws IndexOutOfBoundsException {
		GridLayoutPosition location = this.mPositions.get(position);
		this.pnlLayout.setWidget(location.getRow(), location.getColumn(),
				widget);
		widget.setSize("100%", "100%");
	}

	private final InvalidPatternException createException(char c, int position) {
		String msg = "unexpected " + c + " at " + position;
		return new InvalidPatternException(msg);
	}

	private final void createLayoutGrid(List<List<GridCellDefinition>> gridDef) {
		FlexCellFormatter formatter = this.pnlLayout.getFlexCellFormatter();

		for (List<GridCellDefinition> rowDef : gridDef) {
			for (GridCellDefinition cellDef : rowDef) {
				int row = cellDef.getRow();
				int col = cellDef.getCol();
				if (cellDef.getRowSpan() > 1) {
					formatter.setRowSpan(row, col, cellDef.getRowSpan());
				}
				if (cellDef.getColSpan() > 1) {
					formatter.setColSpan(row, col, cellDef.getColSpan());
				}

				if (cellDef.getStyleName() != null) {
					String styleName = cellDef.getStyleName();
					String align = "";
					if (styleName.contains(":")) {
						int index = styleName.indexOf(":");
						align = styleName.substring(0, index);
						styleName = styleName.substring(index + 1);
					}

					if (align.length() > 0) {
						if (align.contains("L")) {
							formatter.setHorizontalAlignment(row, col,
									HasHorizontalAlignment.ALIGN_LEFT);
						}
						if (align.contains("C")) {
							formatter.setHorizontalAlignment(row, col,
									HasHorizontalAlignment.ALIGN_CENTER);
						}
						if (align.contains("R")) {
							formatter.setHorizontalAlignment(row, col,
									HasHorizontalAlignment.ALIGN_RIGHT);
						}
						if (align.contains("T")) {
							formatter.setVerticalAlignment(row, col,
									HasVerticalAlignment.ALIGN_TOP);
						}
						if (align.contains("M")) {
							formatter.setVerticalAlignment(row, col,
									HasVerticalAlignment.ALIGN_MIDDLE);
						}
						if (align.contains("B")) {
							formatter.setVerticalAlignment(row, col,
									HasVerticalAlignment.ALIGN_BOTTOM);
						}
					}
					formatter.setStyleName(row, col, cellDef.getStyleName());
					if (cellDef.getWidth() != null && cellDef.getWidth().length() > 0) {
						formatter.setWidth(row, col, cellDef.getWidth());
					}
					if (cellDef.getHeight() != null && cellDef.getHeight().length() > 0) {
						formatter.setHeight(row, col, cellDef.getHeight());
					}
				}
			}
		}
	}

	/**
	 * @param pattern
	 * @throws InvalidPatternException
	 */
	private final void parsePattern(String pattern)
			throws InvalidPatternException {
		if (pattern == null) {
			throw new InvalidPatternException("pattern can not be null.");
		}

		PatternReader reader = new PatternReader(pattern);
		int row = 0;
		int column = 0;
		List<List<GridCellDefinition>> gridDef = new ArrayList<List<GridCellDefinition>>();
		List<GridCellDefinition> rowDef = new ArrayList<GridCellDefinition>();
		gridDef.add(rowDef);
		GridCellDefinition cellDef = null;
		StringBuilder cnBuilder = null;
		while (reader.hasMore()) {
			Character c = reader.next();
			if (c.equals('|')) {
				if (cellDef == null) {
					cellDef = new GridCellDefinition();
					cellDef.setCol(column);
					cellDef.setRow(row);
				} else {
					if (cellDef.mCellType == null) {
						throw createException(c, reader.getPosition());
					} else if (cellDef.mCellType.equals("^")) {
						boolean found = false;
						int topRowIndex = row - 1;
						while (topRowIndex >= 0 && topRowIndex < gridDef.size()) {
							List<GridCellDefinition> topRow = gridDef.get(topRowIndex);
							if (topRow.size() < column) {
								break;
							}
							if (topRow.get(column).mCellType.equals("<")) {
								throw createException(c, reader.getPosition());
							} else if (!topRow.get(column).mCellType.equals("^")) {
								int rowSpan = topRow.get(column).getRowSpan();
								topRow.get(column).setRowSpan(rowSpan + 1);
								found = true;
								break;
							} else {
								topRowIndex--;
							}
						}

						if (!found) {
							throw createException(c, reader.getPosition());
						}
					} else if (cellDef.mCellType.equals("<")) {
						boolean found = false;
						int leftColIndex = column - 1;
						while (leftColIndex >= 0
								&& leftColIndex < rowDef.size()) {
							GridCellDefinition leftCol = rowDef.get(leftColIndex);
							if (leftCol.mCellType.equals("^")) {
								throw createException(c, reader.getPosition());
							} else if (!leftCol.mCellType.equals("<")) {
								int colSpan = leftCol.getColSpan();
								leftCol.setColSpan(colSpan + 1);
								found = true;
								break;
							} else {
								leftColIndex--;
							}
						}

						if (!found) {
							throw createException(c, reader.getPosition());
						}
					} else if (cellDef.mCellType.equals("@")) {
						this.mPositions.add(new GridLayoutPosition(row, column));
					}
					rowDef.add(cellDef);
					if (!cellDef.mCellType.equals("^")) {
						column++;
					}
					cellDef = new GridCellDefinition();
					cellDef.setRow(row);
					cellDef.setCol(column);
				}
			} else if (c.equals('$')) {
				if (cellDef == null) {
					throw createException(c, reader.getPosition());
				}
				cellDef = null;
				column = 0;
				row++;
				rowDef = new ArrayList<GridCellDefinition>();
				gridDef.add(rowDef);
			} else {
				if (cellDef == null) {
					throw createException(c, reader.getPosition());
				}
				if (c.equals(' ') || c.equals('\t')) {
					if (cellDef.getCellType() == null) {
						cellDef.setCellType(String.valueOf(c));
					} else {
						// Ignore the space if it does not represents the type
					}
				} else if (c.equals('@') || c.equals('^') || c.equals(' ')
						|| c.equals('<')) {
					if (cellDef.getCellType() != null) {
						throw createException(c, reader.getPosition());
					}

					cellDef.setCellType(String.valueOf(c));
				} else {
					if (!"@".equals(cellDef.getCellType())) {
						throw createException(c, reader.getPosition());
					}

					if (c.equals('{')) {
						if (cellDef.getStyleName() != null || cnBuilder != null) {
							throw createException(c, reader.getPosition());
						}
						cnBuilder = new StringBuilder();
					} else if (c.equals('}')) {
						if (cnBuilder == null || cnBuilder.length() == 0) {
							throw createException(c, reader.getPosition());
						}
						String styleName = cnBuilder.toString();
						if (styleName.length() > 0) {
							String[] styleParts = styleName.split(",");
							cellDef.setStyleName(styleParts[0]);
							if (styleParts.length > 1) {
								cellDef.setWidth(styleParts[1]);
							}
							if (styleParts.length > 2) {
								cellDef.setHeight(styleParts[2]);
							}
						}
						cnBuilder = null;
					} else {
						if (cnBuilder == null) {
							throw createException(c, reader.getPosition());
						}

						cnBuilder.append(c);
					}
				}
			}
		}
		this.createLayoutGrid(gridDef);
	}

	public void addStyleDependentName(int position, String styleSuffix) {
		Widget widget = this.getWidget(position);
		if (widget != null) {
			widget.addStyleDependentName(styleSuffix);
		}
	}
	
	public void setStylePrimaryName(int position, String style) {
		Widget widget = this.getWidget(position);
		if (widget != null) {
			widget.setStylePrimaryName(style);
		}
	}
	
	public void setStyleName(int position, String style) {
		Widget widget = this.getWidget(position);
		if (widget != null) {
			widget.setStyleName(style);
		}
	}
}
